Многопоточное программирование в Java - Тимур Машнин
Шрифт:
Интервал:
Закладка:
Дополнительно, так как поток уже существует, когда создается задача, устраняется задержка, возникающая при создании потока, что делает приложение более отзывчивым.
Кроме того, правильно настроив количество потоков в пуле потоков, вы можете предотвратить переполнение ресурсов, заставляя задачи, превышающие определенный порог, ждать, пока потоки будут доступны для их обработки.
В примере с веб-приложением, в котором необходимо обрабатывать долгие задачи, инициированные пользовательскими запросами, проблема может быть решена путем создания пула потоков при запуске приложения, а затем распределением пользовательских запросов по рабочим потокам пула потоков.
Резюмируя, Threadpool состоит из потоков, которые ищут задания для выполнения.
Вместо запуска нового потока с объектом Runnable, рабочий поток пула потоков просто вызывает функцию run объекта Runnable.
Таким образом, поток в ThreadPool не создается с помощью Runnable, который вы предоставляете, но существующий поток пула потоков просто проверяет, готовы ли какие-либо задачи к выполнению и вызывает их напрямую.
Потоки создаются только один раз в пуле потоков, за исключением случаев, когда из-за какого-то сбоя поток выходит из строя.
Рабочие потоки опрашивают очередь, чтобы увидеть, есть ли задача для выполнения и запускают ее.
Хотя пул потоков является мощным механизмом структурирования многопоточных приложений, он не лишен риска.
Приложения, созданные с использованием пула потоков, подвержены тем же рискам многопоточности как и любое другое многопоточное приложение, например, ошибкам синхронизации и deadlock, а также некоторым другим рискам, характерным для пула потоков, например, deadlock, связанному с самим пулом, переполнения ресурсов и утечек потоков.
В то время как deadlock является риском в любой многопоточной программе, пулы потоков создают еще одну возможность для ситуации deadlock, когда все потоки пула выполняют задачи, которые блокируются в ожидании результатов другой задачи в очереди, но другая задача не может работать, потому что нет свободных потоков пула.
Далее, размер пула потоков должен быть правильно настроен.
Потоки потребляют множество ресурсов, включая память для объекта Thread, стеки выполнения.
Кроме того, JVM, скорее всего, создаст собственный поток для каждого потока Thread, потребляя дополнительные системные ресурсы.
Наконец, есть накладные расходы переключения между потоками.
Если пул потоков слишком велик, ресурсы, потребляемые этими потоками, могут существенно повлиять на производительность системы.
Значительный риск для пулов потоков — это утечка потока, которая возникает, когда поток удаляется из пула для выполнения задачи, но не возвращается в пул, когда задача завершается.
Это может произойти, если задача выбросит исключение.
Если объект пула потока не перехватит исключение и восполнит поток, размер пула потоков будет уменьшен на единицу.
Если это будет происходить постоянно, пул потоков в конечном итоге будет пустым, и система остановится, потому что не будет доступных потоков для обработки задач.
Задачи, которые постоянно останавливаются, например, задачи, которые долго ожидают внешние ресурсы, также могут вызывать эквивалент утечки потока.
Если поток постоянно потребляется такой задачей, он эффективно удаляется из пула.
Такие задачи должны либо обрабатываться потоком не из пула, либо ожидать ограниченное время.
Для эффективного использования пула потоков, не ставьте в очередь задачи, которые ждут синхронно результатов других задач.
Это может вызвать deadlock, когда все потоки заняты задачами, которые в свою очередь ожидают результатов от задач, поставленных в очередь, которые не могут выполняться, потому что все потоки заняты.
При использовании потоков пула для потенциально долгоживущих операций, и, если программа должна дождаться какого-либо ресурса, например, завершения ввода-вывода, определите максимальное время ожидания, а затем завершите и повторите задачу для выполнения позже. Это освободит поток для выполнения другой задачи.
Если у вас разные типы задач с отличающимися характеристиками, имеет смысл создать несколько пулов с соответствующими характеристиками.
Для чисто вычислительных задач имеет смысл определить размер пула равным количеству процессоров системы.
Для сетевых задач размер пула можно оценить, как N* (1+WT/ST) (N-количество процессоров, WT-время ожидания ресурса, ST-время обслуживания запроса).
В этом примере создается пул из одного потока и неограниченной очереди.
И если это многопользовательское приложение, тогда при каждом запросе, создающем объект Runnable, этот код будет помещать новый объект Runnable в очередь на выполнение единственным потоком из пула.
Здесь пул потока создается отдельно при запуске приложения.
И отдельно при остановке приложения вызывается метод shutdown, который перестает принимать новые задачи, ждет выполнения ранее поставленных задач, а затем завершает работу executor.
Можно вызвать метод shutdownNow, который прерывает все выполняемые задачи и немедленно завершает работу executor.
Хороший способ закрыть ExecutorService, это использовать оба этих методов в сочетании с методом awaitTermination.
При таком подходе ExecutorService сначала прекратит принимать новые задачи, подождет определенный период времени для завершения всех задач.
Если в течении этого времени задачи не завершатся, вызвать метод shutdownNow, который прервет все выполняемые задачи и немедленно завершит работу ExecutorService.
Здесь, в этом примере, та же самая ситуация, только одновременно создаются две задачи, которые помещаются в очередь на выполнение.
Вместо использования объекта Runnable, так как использование Runnable ограничено тем, что задачи не могут вернуть результат, для создания задачи можно использовать объект Callable, который позволяет задаче вернуть результат.
Если в Runnable мы определяем метод run, то в Callable, мы определяем метод call.
Метод submit помещает задачу в очередь для выполнения потоком.
Однако он не знает, когда будет получен результат этой задачи.
Поэтому он возвращает специальный тип значения, называемый Future, который может использоваться для извлечения результата задачи, когда этот результат будет доступен.
Метод ExecutorService.submit немедленно возвращает объект Future.
После того, как вы получили Future, вы можете выполнять другие задачи параллельно во время выполнения поставленной задачи, а затем использовать метод future.get для получения результата.
Обратите внимание, метод get устанавливает блокировку текущего потока до тех пор, пока не завершится выполнение задачи.
Future также предоставляет метод isDone для проверки того, завершена задача или нет.
Если прерывается поток, выполняющий вычисление, метод генерирует исключение InterruptedException.
Когда вычисление задачи завершается, метод get немедленно возвращает управление.
Вы можете отменить Future, используя метод Future.cancel, который пытается отменить выполнение задачи и возвращает true, если она отменена успешно, иначе метод возвращает false.
Метод cancel принимает логический аргумент, и если вы передадите значение true для этого аргумента, то поток, который в